[MyBatis] 대용량 트랜잭션 매핑과 동적 SQL 성능 최적화 패턴
1. 고성능 소켓 서버와 RDBMS 간의 임피던스 불일치
Netty는 Non-blocking 이벤트 루프 기반으로 초당 수만 건의 소켓 패킷을 가볍게 수신하지만, 뒷단의 RDBMS(MySQL, Oracle)는 물리적 디스크 I/O와 커넥션 풀(HikariCP)의 제약을 받습니다.
만약 Netty의 I/O Worker 스레드 내부에서 직접 MyBatis Mapper를 동기(Blocking)로 호출하면 다음과 같은 치명적인 병목이 발생합니다:
- DB 쿼리 응답이 100ms 지연될 때 Worker 스레드가 블로킹되어 다른 수천 개 소켓의 이벤트 처리가 중단됨.
- 커넥션 풀 고갈(
ConnectionTimeoutException) 발생.
해결 아키텍처:
- I/O Worker 스레드는 패킷 검증 후 비즈니스 DTO를 즉시 큐나 비동기 워커 스레드 풀(Executor)로 위임하고, DB 영속화는 별도의 스레드 풀에서 전담하도록 분리해야 합니다.
flowchart LR NettyWorker["Netty I/O Worker Thread<br/>(절대 블로킹 금지)"] -->|Event 위임| Queue["LinkedBlockingQueue / TaskExecutor"] Queue --> DBWorker["DB Persistence Worker Thread"] DBWorker --> MyBatis["SqlSession (ExecutorType.BATCH)"] MyBatis --> RDBMS[("MySQL / Oracle DB")]
2. 동적 SQL 작성 시 실행 계획(Execution Plan) 왜곡 방지
2.1 <if> 태그 남발로 인한 옵티마이저 오작동
<!-- 안티 패턴: 검색 조건 조합에 따라 완전히 다른 SQL 생성 -->
<select id="findTransactions" resultType="TxDto">
SELECT * FROM HA_TRANSACTION
WHERE 1=1
<if test="mid != null and mid != ''">
AND mid = #{mid}
</if>
<if test="startDate != null">
AND tx_date >= #{startDate}
</if>
<if test="status != null">
AND status = #{status}
</if>
</select>- 파라미터 유무에 따라 인덱스 결합 조건이 깨지며 옵티마이저가 인덱스 풀 스캔(Index Full Scan)이나 테이블 풀 스캔(Table Full Scan)을 선택하게 됩니다.
- 해결책:
- 자주 호출되는 핵심 트랜잭션 조회는 복합 인덱스(
(mid, tx_date, status))를 명확하게 타도록 전용 쿼리 메서드로 분리하는 것이 좋습니다.
- 자주 호출되는 핵심 트랜잭션 조회는 복합 인덱스(
3. 대용량 로그·텔레메트리 일괄 삽입 (ExecutorType.BATCH)
단말기 헬스체크 로그나 IoT 센서 데이터를 건건이 INSERT하면 1,000건 삽입 시 1,000번의 네트워크 라운드 트립(Round-trip)이 발생합니다.
3.1 MyBatis Batch Executor 활용:
@Service
@RequiredArgsConstructor
public class TelemetryBatchService {
private final SqlSessionFactory sqlSessionFactory;
public void insertBatch(List<DeviceLogDto> logList) {
// BATCH 모드의 SqlSession 오픈
try (SqlSession sqlSession = sqlSessionFactory.openSession(ExecutorType.BATCH, false)) {
AppMapper mapper = sqlSession.getMapper(AppMapper.class);
for (int i = 0; i < logList.size(); i++) {
mapper.insertDeviceLog(logList.get(i));
// 500건 단위로 flush하여 힙 메모리 급증 방지
if (i > 0 && i % 500 == 0) {
sqlSession.flushStatements();
}
}
// 잔여 데이터 flush 및 커밋
sqlSession.flushStatements();
sqlSession.commit();
}
}
}- JDBC 드라이버의
rewriteBatchedStatements=true옵션과 결합하면, 단일 TCP 패킷 내에 다중 행INSERT INTO ... VALUES (...), (...)형태로 묶여 전송되므로 처리 성능이 10~20배 향상됩니다.\n